﻿using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Threading;

namespace EcclesCollege.Tools
{
	/// <summary>
	/// Thread-safe dictionary with simple expiry, useful for caching
	/// </summary>
	public class SafeDict<K,V> : IDisposable
	{
		private ReaderWriterLockSlim locker;
		private Dictionary<K,V> dict;
		private DateTime expires;
		private int lifetimemins;
		private int maxelements;

		private const int coLockTimeout = 1000; // milliseconds

		public SafeDict(int initialsize, int maxsize, int mins)
		{
			lifetimemins = mins;
			maxelements = maxsize;
			locker = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
			dict = new Dictionary<K, V>(initialsize);
			expires = DateTime.Now.AddMinutes(mins);
		}

		/// <summary>
		/// Dictionary is allowed to grow to twice its original size
		/// </summary>
		public SafeDict(int initialsize, int mins)
			: this(initialsize, initialsize * 2, mins)
		{
			if (initialsize < 5)
				maxelements = 30;	// just to stop maxelements getting anything stupid like zero
		}

		/// <summary>
		/// Dictionary with default size of 50, timeout after 30 mins
		/// </summary>
		public SafeDict()
			: this(50, 200, 30)
		{
		}

		/// <summary>
		/// Add a value, just replaces any existing values. Inforces expiry. Gets Write Lock
		/// </summary>
		public void SetValue(K key, V value)
		{
			GetWriteLock();
			try
			{
				if (maxelements > 0 && dict.Count > maxelements)
					EmptyInternal();

				dict[key] = value;
			}
			finally
			{
				locker.ExitWriteLock();
			}
		}

		/// <summary>
		/// Remove all existing contents from the dictionary. Gets Write Lock
		/// </summary>
		public void EmptyDict()
		{
			GetWriteLock();
			try
			{
				EmptyInternal();
			}
			finally
			{
				locker.ExitWriteLock();
			}
		}

		/// <summary>
		/// MUST only be used when a write lock has already be gained
		/// </summary>
		private void EmptyInternal()
		{
			expires = DateTime.Now.AddMinutes(lifetimemins);
			dict.Clear();
		}

		/// <summary>
		/// Number of elements. Gets Read Lock
		/// </summary>
		public int Count
		{
			get
			{
				GetReadLock();
				try
				{
					return dict.Count;
				}
				finally
				{
					locker.ExitReadLock();
				}
			}
		}

		/// <summary>
		/// Get specified value, using default value if not found. Optionally add default item to dictionary. Inforces expiry. Gets Read Lock, and potentially Write Lock
		/// </summary>
		public V GetValue(K key, V defvalue, bool addifnotfound)
		{
			V info;
			bool found = false;
			DateTime exp;

			GetReadLock();
			try
			{
				if (dict.TryGetValue(key, out info))
					found = true;

				exp = expires;
			}
			finally
			{
				locker.ExitReadLock();
			}

			if (exp < DateTime.Now)
				EmptyDict();

			if (found) return info;

			// not found
			if (addifnotfound)
				SetValue(key, defvalue);
			return defvalue;
		}

		/// <summary>
		/// TryGetValue - checks expiry date. Inforces expiry. Gets Read Lock and potentially Write Lock
		/// </summary>
		public bool TryGetValue(K key, out V result)
		{
			bool found = false;
			DateTime exp;

			GetReadLock();
			try
			{
				if (dict.TryGetValue(key, out result))
					found = true;
				exp = expires;
			}
			finally
			{
				locker.ExitReadLock();
			}

			if (exp < DateTime.Now)
				EmptyDict();

			return found;
		}

		/// <summary>
		/// Returns all keys in dictionary. Gets Read Lock
		/// </summary>
		public K[] GetKeys()
		{
			GetReadLock();
			try
			{
				K[] res = new K[dict.Count];
				dict.Keys.CopyTo(res, 0);
				return res;
			}
			finally
			{
				locker.ExitReadLock();
			}
		}

		/// <summary>
		/// Internal
		/// </summary>
		private void GetReadLock()
		{
			if (!locker.TryEnterReadLock(coLockTimeout))
				throw new SafeDictException("Lock timeout - read");
		}

		/// <summary>
		/// Internal
		/// </summary>
		private void GetWriteLock()
		{
			if (!locker.TryEnterWriteLock(coLockTimeout))
				throw new SafeDictException("Lock timeout - write");
		}

		public void Dispose()
		{
			if (locker != null)
				locker.Dispose();
		}
	}

	public class SafeDictException : Exception
	{
		public SafeDictException(string msg)
			: base(msg)
		{
		}
	}
}
